package library;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.spec.MGF1ParameterSpec;
import java.util.ArrayList;
import java.util.Date;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Map.Entry;

import db.LibraryDb;
import db.LibraryDbExporter;
import db.LibraryDbLoader;
import dom.EducationalSystemEnum;
import udp.UdpClient;
import udp.UdpServer;
import udp.message.*;
import util.ConfigurationHelper;
import client.CommandClient;


/**
 * Encapsulates the library server
 * @author Alex Perkins
 *
 */
public class LibraryServerContainer extends UdpServer {

	private boolean heartbeatActive;
	protected static String replicaId = "";
	protected static int port;
	private boolean debugMode = Boolean.parseBoolean(ConfigurationHelper.getProperty("debug"));
	private Hashtable<String,Date> heartbeatHistory;
	LibraryUtil libraryUtil = new LibraryUtil();
	// Library server instances
	LibraryServer concordia = null;
	LibraryServer mcgill = null;
	LibraryServer uqam = null;
	// Thread for heartbeat
	Thread heartbeatThread = null;
	Thread verifyHeartbeatThread = null;
	
	
	public LibraryServerContainer() {
		super(replicaId,port);
		heartbeatHistory = new Hashtable<String,Date>();
		startUDPServer();
		startHeartbeat();
		startVerifyingHeartbeats();
	}
	
	/**
	 * ONLY USE FOR TESTING 
	 * @param replicaId
	 */
	public LibraryServerContainer(String replicaId){
		super(replicaId,5000);
		this.replicaId = replicaId;
		createServerInstance("all");
	}

	public static void main(String[] args){
		replicaId = args[0];
		port = ConfigurationHelper.getIntProperty(replicaId + ".replica.port");
		LibraryServerContainer test = new LibraryServerContainer();
		CommandClient.printHeader("Replica " + replicaId);
	}


	@Override
	public void receiveMessage(UdpMessage message) {

		//System.out.println("LibraryServerContainer receiveMessage");
		if(message != null){
			
			if(message instanceof FailMessage) {
				// FailMessage simulates a crash or a software failure

				FailMessage failMessage = (FailMessage) message;
				
				if (failMessage.getFailureType().equals("crash")) {
					
					// Failure type: Crash failure
					//add stop heartbeat
					stopHeartbeat();
					concordia.stopServer();
					mcgill.stopServer();
					uqam.stopServer();
					print("Crash failure. Library instances stopped.");
					
				} else if (failMessage.getFailureType().equals("software")) {
					
					// Failure type: Software failure
					concordia.nullifyDatabase();
					mcgill.nullifyDatabase();
					uqam.nullifyDatabase();
					print("Sofware failure. Library databases nullified.");
					
				}
				
			}
			
			else if(message instanceof LibraryMessage){
				// TODO: check if we need to use a sequence number here
				LibraryMessage msg = (LibraryMessage) message;
				handleLibraryCommand(msg);
				//checksequence(message);
				debug("Not the right place for these commands");
			}
			else if(message instanceof StartLibraryMessage) {
				//StartLibraryMessage startLibraryMessage = (StartLibraryMessage) message; 
				createServerInstance("all");
			}
			else if(message instanceof SyncLibraryDatabaseMessage){
				SyncLibraryDatabaseMessage syncLibraryDatabaseMessage = (SyncLibraryDatabaseMessage) message; 
				processSyncLibraryDatabaseMessage(syncLibraryDatabaseMessage);
			}	 
			else if (message instanceof GetLatestDatabaseMessage) {
				GetLatestDatabaseMessage getLatestDatabaseMessage = (GetLatestDatabaseMessage) message;
				processGetLatestDatabaseMessage(getLatestDatabaseMessage);
			}
			else if(message instanceof HeartbeatMessage){ // DOES NOT APPLY TO SEQUENCE NUMBER
				HeartbeatMessage heartbeatMessage = (HeartbeatMessage) message;
				// Add the library and heartbeat info with when you received it
				//validateLibraryMessage(message);
				processHeartbeatMessage(heartbeatMessage);
				//heartbeatHistory.put(message.getSrcLibrary(), new Date());
				//debug("updated heartbeat info");
			}
			else if (message instanceof FailMessage) {
				print("Received Fail Message");
				// TODO fail
				//pendingFail = true;
			}
			else{
				String msg = "unknown UdpMessage instance [";
				if(message.getClass()!=null && message.getClass().getName() != null){
					msg += message.getClass().getName();
				}
				msg += "]";
				print(msg);
			}
		}
		else{
			print("received a null LibraryMessage");
		}

	}


	/**
	 * Process the command
	 * @param message
	 */
	public void handleLibraryCommand(LibraryMessage message){

		debug("received command: " + message.toString());

		if(message instanceof CreateAccountMessage){
			print("Should not be receiving CreateAccountMessage at this level");
		}
		else if(message instanceof GetMissingRequestMessage){
			//GetMissingRequestMessage getMissingRequestMessage = (GetMissingRequestMessage) message;
			//processGetMissingRequestMessage(getMissingRequestMessage);
		}
		else if(message instanceof GetNonReturnersMessage){
			print("Should not be receiving GetNonReturnersMessage at this level");
		}
		else if(message instanceof ReserveBookInterMessage){
			print("Should not be receiving ReserveBookInterMessage at this level");
		}
		else if(message instanceof ReserveBookMessage){
			print("Should not be receiving ReserveBookMessage at this level");
		}
		else if(message instanceof SetDurationMessage){
			print("Should not be receiving SetDurationMessage at this level");
		}
		

		else{
			print("Unrecognized command: " + message.toString());
		}
	}

	/**
	 * Handles the actions required to be taken when a heartbeat message is received
	 * @param heartbeatMessage
	 */
	private synchronized void processHeartbeatMessage(HeartbeatMessage heartbeatMessage){

		Date now = new Date();

		heartbeatHistory.put(heartbeatMessage.getSrcReplicaId(),now);

		debug("==== RECEIVED HEARTBEAT =================================");
		debug("Setting Date [" + heartbeatMessage.getSrcReplicaId() + "]: " + libraryUtil.dateFormatter.format(now));
		debug("=========================================================");

	}

	/**
	 * Creates the server instance
	 * @param edu
	 */
	private void createServerInstance(String edu){

		edu = edu.toUpperCase();

		try{

			if(edu.equalsIgnoreCase("all")){
				concordia = new LibraryServer(EducationalSystemEnum.CONCORDIA,replicaId);
				mcgill = new LibraryServer(EducationalSystemEnum.MCGILL,replicaId);
				uqam = new LibraryServer(EducationalSystemEnum.UQAM,replicaId);
			}
			else if(edu.equalsIgnoreCase("concordia")){
				concordia = new LibraryServer(EducationalSystemEnum.valueOf(edu),replicaId);
			}
			else if(edu.equalsIgnoreCase("mcgill")){
				mcgill = new LibraryServer(EducationalSystemEnum.valueOf(edu),replicaId);
			}
			else if(edu.equalsIgnoreCase("uqam")){
				uqam = new LibraryServer(EducationalSystemEnum.valueOf(edu),replicaId);
			}
			else{
				print("can't start library server '" + edu + "' since it doesn't exist");
			}

		}
		catch(Exception e){
			print("there was a problem creating library instance. Cause: " + e.getMessage());
		}
	}
	
	private void restartServerInstances(){
		//concordia
		//mcgill
		//uqam
	}

	private void stopServerInstance(String edu){

		edu = edu.toUpperCase();

		try{

			if(edu.equalsIgnoreCase("all")){
				concordia.stopServer();
				//concordia = null;
				mcgill.stopServer();
				//mcgill = null;
				uqam.stopServer();
				//uqam = null;
			}
			else if(edu.equalsIgnoreCase("concordia")){
				concordia.stopServer();
				concordia = null;
			}
			else if(edu.equalsIgnoreCase("mcgill")){
				mcgill.stopServer();
				mcgill = null;
			}
			else if(edu.equalsIgnoreCase("uqam")){
				uqam.stopServer();
				uqam = null;
			}
			else{
				print("can't stop library server '" + edu + "' since it doesn't exist");
			}

		}
		catch(Exception e){
			print("problem creating library instance");
		}
	}

	/**
	 * Handles sending heartbeats to the other servers
	 */
	private void startHeartbeat(){

		this.heartbeatActive=true;
		
		debug("Going to start sending heart beats now");
		
		heartbeatThread = new Thread() {
			
			public void run() {

				Timer timer = new Timer();

				int milliseconds = ConfigurationHelper.getIntProperty("heartbeat.milliseconds");

				timer.scheduleAtFixedRate(new TimerTask() {
					@Override
					public synchronized void run() {
						if(heartbeatActive){
							//	TODO: add a heart-beat here DONE
							HeartbeatMessage command = new HeartbeatMessage();
							// where the heartbeat came from
							command.setSrcReplicaId(replicaId);
							//debug("sending heartbeat to other replicas");
							// multicast the heartbeat to all servers
							multicast(command);	  
							// check for late library servers
						}
					}
				}, milliseconds, milliseconds);
			}
		};

		// start sending heart-beats
		heartbeatThread.start();
	}
	
	public void stopHeartbeat(){
		this.heartbeatActive=false;
		if(heartbeatThread != null){
			heartbeatThread.stop();
		}
		if(verifyHeartbeatThread != null){
			verifyHeartbeatThread.stop();
		}
	}

	/**
	 * Verifies heart-beats in a timer
	 */
	private void startVerifyingHeartbeats(){

		debug("going to start checking the heatbeats now");
		
		verifyHeartbeatThread = new Thread() {

			public void run() {

				Timer timer = new Timer();

				long milliseconds = ConfigurationHelper.getIntProperty("heartbeat.milliseconds");
				milliseconds = (milliseconds/2);

				timer.scheduleAtFixedRate(new TimerTask() {
					@Override
					public void run() {
						// check for late library servers
						if(heartbeatActive){
							verifyHeartbeats();
						}
					}
				}, milliseconds, milliseconds);
			}
		};

		// start verifying heart-beats
		verifyHeartbeatThread.start();
	}

	/**
	 * verify the heart-beats
	 */
	private void verifyHeartbeats(){

		int threshold = ConfigurationHelper.getIntProperty("heartbeat.threshold");

		debug("checking heartbeats with threshold : " + threshold + " ms.");
		
		if(heartbeatHistory.isEmpty()){
			debug("heartbeat history is empty");
		}
		
		for(Entry<String, Date> entry : heartbeatHistory.entrySet()){

			//debug("checking [" + entry.getKey() + "] for heartbeat info");

			if(libraryUtil.isWithinInterval(entry.getValue(), threshold)){
				debug("replica [" + entry.getKey() + "] is responding");
			}
			else{
				
				print("replica [" + entry.getKey() + "] is not responding");
				
				//TODO: report the issue to the replica manager
				ReportReplicaMessage reportReplicaMessage = new ReportReplicaMessage(replicaId,entry.getKey());
				reportReplicaMessage.setDestHost(ConfigurationHelper.getProperty("replica.manager.host"));
				reportReplicaMessage.setDestPort(ConfigurationHelper.getIntProperty("replica.manager.port"));
				send(reportReplicaMessage);
			}
		}

	}

	/**
	 * Handles the actions required to sync the databases to a good one.
	 * @param syncDatabaseLibraryMessage
	 * @throws UnknownHostException 
	 * @throws SocketException 
	 */
	private void processSyncLibraryDatabaseMessage(
			SyncLibraryDatabaseMessage syncLibraryDatabaseMessage) {

	
		this.stopServerInstance("all");
		
		/*
		 * Sends message to one replica at a time, if it doesn't get an answer within 10 seconds,
		 * it will send a message to the next replica. If it gets a reply on time, it will break 
		 * of loop.
		 */
		
		GetLatestDatabaseMessage message = new GetLatestDatabaseMessage();
		message.setSrcHost(ConfigurationHelper.getProperty(replicaId + ".replica.host"));
		message.setSrcPort(ConfigurationHelper.getIntProperty(replicaId + ".replica.port"));
		String[] replicas = {"r1", "r2", "r3", "r4"};

		GetLatestDatabaseMessage reply = null;

		for(String r: replicas) {	
			if(!r.equals(this.replicaId)) {
				debug("sending message to " + r);
				UdpClient currentReplica = null;
				try {
					currentReplica = new UdpClient(ConfigurationHelper.getProperty(r+".replica.host"), ConfigurationHelper.getIntProperty(r+ ".replica.port"));

				} catch (SocketException | UnknownHostException e1) {
					e1.printStackTrace();
				}
				try {

					debug("sending and receiving message to " + r);
					reply = (GetLatestDatabaseMessage) currentReplica.sendAndReceive(message, 3000);
					debug("sendAndReceive completed: received message from " + r);


					break;	

				} catch(SocketTimeoutException e) {
					debug("Fail get latest database " + r);
					continue;
				
				}
			}
		}

		
		if (reply instanceof GetLatestDatabaseMessage) {
			
			LibraryDbExporter libraryDbExporter = new LibraryDbExporter();
			
			PrintWriter writer; 
			
			try {
				System.out.println(reply.getConcordiaAccounts());
				libraryDbExporter.writeStringToFile(reply.getConcordiaAccounts(),ConfigurationHelper.getProperty(replicaId+".concordia.accounts.db"));
				libraryDbExporter.writeStringToFile(reply.getConcordiaBooks(), ConfigurationHelper.getProperty(replicaId+".concordia.books.db"));
				
				libraryDbExporter.writeStringToFile(reply.getMcgillAccounts(), ConfigurationHelper.getProperty(replicaId+".mcgill.accounts.db"));
				libraryDbExporter.writeStringToFile(reply.getMcgillBooks(), ConfigurationHelper.getProperty(replicaId+".mcgill.books.db"));
				
				libraryDbExporter.writeStringToFile(reply.getUqamAccounts(), ConfigurationHelper.getProperty(replicaId+".uqam.accounts.db"));
				libraryDbExporter.writeStringToFile(reply.getUqamAccounts(), ConfigurationHelper.getProperty(replicaId+".uqam.books.db"));
				
			} catch (FileNotFoundException e) {
				
				e.printStackTrace();
			}
			
			this.createServerInstance("all");
			
		} 
		else {
			if (reply == null) {
				print("processSyncLibraryMessage: ** Did not get a database reply from any replica **"); 
			} else {
				print("processSyncLibraryMessage: Unrecognized reply: " + reply.toString());
			}
		}
		//restart heartbeat
		if(!this.heartbeatActive){
			startHeartbeat();
		}

	}

	private void processGetLatestDatabaseMessage(
			GetLatestDatabaseMessage getLatestDatabaseMessage) {		
		//TODO: library server name!
		debug("Received get latest database message");
		
		GetLatestDatabaseMessage reply = new GetLatestDatabaseMessage();
		
		//CONCORDIA BOOKS
		LibraryDbLoader libraryDbLoader = new LibraryDbLoader();
		debug("replica ID : " + replicaId +" @@@@@@@@@@@@@@@@");
		String concordiaBooks = libraryDbLoader.getData(ConfigurationHelper.getProperty(replicaId+".concordia.books.db"));
		reply.setConcordiaBooks(concordiaBooks);
		
		//CONCORDIA ACCOUNTS
		String  concordiaAccounts = libraryDbLoader.getData(ConfigurationHelper.getProperty(replicaId+".concordia.accounts.db"));
		reply.setConcordiaBooks(concordiaAccounts);
		debug("CONCORDIA ACCOUNTS: " + concordiaAccounts);
		debug("CONCORDIA ACCOUNTS: " + reply.getConcordiaAccounts());
		//MCGILL
		String  mcgillAccounts = libraryDbLoader.getData(ConfigurationHelper.getProperty(replicaId+".mcgill.accounts.db"));
		reply.setMcgillBooks(mcgillAccounts);
		
		//MCGILL
		String  mcgillBooks = libraryDbLoader.getData(ConfigurationHelper.getProperty(replicaId+".mcgill.books.db"));
		reply.setMcgillBooks(mcgillBooks);
		
		//UQAM
		String  uqamAccounts = libraryDbLoader.getData(ConfigurationHelper.getProperty(replicaId+".uqam.accounts.db"));
		reply.setUqamBooks(uqamAccounts);
		
		//UQAM
		String  uqamBooks = libraryDbLoader.getData(ConfigurationHelper.getProperty(replicaId+".uqam.books.db"));
		reply.setUqamBooks(uqamBooks);
		
		reply.setDestPort(getLatestDatabaseMessage.getSocketPort());
		reply.setDestHost(getLatestDatabaseMessage.getSrcHost());

		UdpClient currentReplica = null;
		try {
			currentReplica = new UdpClient(getLatestDatabaseMessage.getSrcHost(), getLatestDatabaseMessage.getSocketPort());
			debug("sending message:  srcHost:" + getLatestDatabaseMessage.getSrcHost() +" socketPort:" + getLatestDatabaseMessage.getSocketPort());
			currentReplica.send(reply);
		} catch (SocketException | UnknownHostException e1) {
			e1.printStackTrace();
		}

	}

	/**
	 * Sends a UDP message to the other library servers
	 * @param message
	 */
	public void multicast(UdpMessage message){

		for(String key : LibraryProperties.getReplicaPortData().keySet()){
			if(!key.toString().equals(replicaId)){
				try{
					message.setDestHost(key.toString());
					String host = LibraryProperties.getReplicaHostData().get(key);
					int port = LibraryProperties.getReplicaPortData().get(key);
					//debug("using [" + host + ":"+port + "]");
					UdpClient client = new UdpClient(host,port);
					client.send(message);
				}
				catch(Exception ex){
					print("Couldn't send message: " + ex.getMessage());
				}
			}
		}

	}

	
	/**
	 * Takes care of sending a message using the udp client
	 * @param msg
	 */
	private void send(UdpMessage msg){
		try{
			// set the src information before sending
			msg.setSrcHost(LibraryProperties.getReplicaHostData().get(replicaId));
			msg.setSrcPort(LibraryProperties.getReplicaPortData().get(replicaId));
			// create the udp client
			UdpClient udpClient = new UdpClient(msg.getDestHost(),msg.getDestPort());
			// send the message
			udpClient.send(msg);
		}
		catch(UnknownHostException e){
			print("uknown host:" + e.getMessage());
		}
		catch(SocketException e){
			print("couldn't connect to socket:" + e.getMessage());
		}
	}
	
	
	/**
	 * Checks if the server is in debug mode before printing to console
	 * @param msg
	 */
	private void debug(String msg){
		if(debugMode){
			print(msg);
		}
	}

	/**
	 * Prints
	 * @param msg
	 */
	private void print(String msg){
		System.out.println("["+id+"] " + msg);
	}
	
	public LibraryServer getMcgill(){
		return mcgill;
	}
	public LibraryServer getConcordia(){
		return concordia;
	}
	public LibraryServer getUqam(){
		return uqam;
	}
	
}
